﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace PhoneNetTools.Ntp
{
    /// <summary>
    /// An SNTP client implementation that allows querying an internet time server.
    /// </summary>
    public class SntpClient : IDisposable
    {
        //// TODO: add a way to abort pending operations
        
        private Socket _udpSocket;
        private SocketAsyncEventArgs _socketEventArgs;
        
        #region Convenience methods

        /// <summary>
        /// A convenient method that builds an NTP message suitable to query an internet time server for its time
        /// and sends the query to the given remote server.
        /// </summary>
        /// <param name="timeserver">The endpoint of the time server to use.</param>
        /// <returns>An awaitable task that eventually returns an Sntp message.</returns>
        public Task<Message> GetTimeAsync(EndPoint timeserver)
        {
            var message = new Message();
            message.Mode = Mode.Client;

            // set the transmit timestamp;
            // the server will copy this timestamp to the "OriginTimestamp" field in its reply
            message.TransmitTimestamp = DateTime.Now.ToUniversalTime();
            return SendRequestAsync(timeserver, message);
        }

        /// <summary>
        /// A convenient method that builds an NTP message suitable to query an internet time server for its time
        /// and sends the query to the given remote server.
        /// </summary>
        /// <param name="timeserverHostName">The host name of the time server to use.</param>
        /// <returns>An awaitable task that eventually returns an Sntp message.</returns>
        public Task<Message> GetTimeAsync(string timeserverHostName)
        {
            var endPoint = new DnsEndPoint(timeserverHostName, Constants.NtpPort);
            return GetTimeAsync(endPoint);
        }

        /// <summary>
        /// A convenient method that builds an NTP message suitable to query an internet time server for its time
        /// and sends the query to the given remote server.
        /// </summary>
        /// <param name="timeserverAddress">The IP address of the time server to use.</param>
        /// <returns>An awaitable task that eventually returns an Sntp message.</returns>
        public Task<Message> GetTimeAsync(IPAddress timeserverAddress)
        {
            var endPoint = new IPEndPoint(timeserverAddress, Constants.NtpPort);
            return GetTimeAsync(endPoint);
        }

        #endregion

        /// <summary>
        /// A method that allows to send an externally build and configured NTP message to a remote server.
        /// </summary>
        /// <param name="timeserver">The endpoint of the time server to use.</param>
        /// <param name="message">The pre-build and configured message to send.</param>
        /// <returns>An awaitable task that eventually returns an Sntp message.</returns>
        public Task<Message> SendRequestAsync(EndPoint timeserver, Message message)
        {
            if (timeserver == null)
            {
                throw new ArgumentNullException("timeserver");
            }

            if (message == null)
            {
                throw new ArgumentNullException("message");
            }

            CleanUp();

            var tcs = new TaskCompletionSource<Message>();

            // this is not pretty, but simplifies the syntax a lot
            EventHandler<SocketAsyncEventArgs> completedHandler = null;
            completedHandler = (sender, e) =>
                                   {
                                       var socket = sender as Socket;

                                       if (socket == null || e == null)
                                       {
                                           CleanUp();

                                           var exception = new NtpException("No result received");
                                           tcs.SetException(exception);

                                           return;
                                       }

                                       // first check for errors
                                       if (e.SocketError != SocketError.Success)
                                       {
                                           CleanUp();

                                           var exception = new NtpException("An error occurred: " + e.SocketError, e.SocketError);
                                           tcs.SetException(exception);

                                           return;
                                       }

                                       // check to see what we need to do next
                                       switch (_socketEventArgs.LastOperation)
                                       {
                                           case SocketAsyncOperation.SendTo:

                                               // sending the request was successful, now reconfigure for receiving the response
                                               e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, Constants.NtpPort);

                                               try
                                               {
                                                   if (!socket.ReceiveFromAsync(_socketEventArgs))
                                                   {
                                                       completedHandler(socket, e);
                                                   }
                                               }
                                               catch (Exception ex)
                                               {
                                                   CleanUp();

                                                   tcs.SetException(ex);
                                               }

                                               break;
                                           case SocketAsyncOperation.ReceiveFrom:

                                               // receiving the response was successful => cleanup and prepare data
                                               CleanUp();

                                               // we also set the destination timestamp
                                               var result = new Message(e.Buffer);
                                               result.DestinationTimestamp = DateTime.Now.ToUniversalTime();

                                               // notify the outside world
                                               tcs.SetResult(result);

                                               break;
                                       }
                                   };

            try
            {
                _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                _socketEventArgs = new SocketAsyncEventArgs();
                _socketEventArgs.Completed += completedHandler;

                // set the remote endpoint and then send the actual data
                _socketEventArgs.RemoteEndPoint = timeserver;
                var buffer = message.ToRawData();
                _socketEventArgs.SetBuffer(buffer, 0, buffer.Length);

                if (!_udpSocket.SendToAsync(_socketEventArgs))
                {
                    completedHandler(_udpSocket, _socketEventArgs);
                }
            }
            catch (Exception ex)
            {
                CleanUp();

                tcs.SetException(ex);
            }

            return tcs.Task;
        }

        /// <summary>
        /// A method that allows to send an externally build and configured NTP message to a remote server.
        /// </summary>
        /// <param name="timeserverHostName">The host name of the time server to use.</param>
        /// <param name="message">The pre-build and configured message to send.</param>
        /// <returns>An awaitable task that eventually returns an Sntp message.</returns>
        public Task<Message> SendRequestAsync(string timeserverHostName, Message message)
        {
            var endPoint = new DnsEndPoint(timeserverHostName, Constants.NtpPort);
            return SendRequestAsync(endPoint, message);
        }

        /// <summary>
        /// A method that allows to send an externally build and configured NTP message to a remote server.
        /// </summary>
        /// <param name="timeserverAddress">The IP address of the time server to use.</param>
        /// <param name="message">The pre-build and configured message to send.</param>
        /// <returns>An awaitable task that eventually returns an Sntp message.</returns>
        public Task<Message> SendRequestAsync(IPAddress timeserverAddress, Message message)
        {
            var endPoint = new IPEndPoint(timeserverAddress, Constants.NtpPort);
            return SendRequestAsync(endPoint, message);
        }
        
        private void CleanUp()
        {
            // release all resources potentially acquired by the event args and socket
            if (_socketEventArgs != null)
            {
                _socketEventArgs.Dispose();
                _socketEventArgs = null;
            }

            if (_udpSocket != null)
            {
                _udpSocket.Close();
                _udpSocket = null;
            }
        }

        #region IDisposable

        private bool _isDisposed;

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                if (disposing)
                {
                    CleanUp();
                }

                // Free your own state (unmanaged objects).
                // Set large fields to null.
                _isDisposed = true;
            }
        }

        #endregion
    }
}